import fg from 'fast-glob';
import fs from 'fs-extra';
import ini from 'ini';
import moment from "moment";
import np from 'normalize-path';
import path from "path";
import wait from 'wait';
import xml2js from "xml2js";
import lodash from "lodash";
import { findFiles, makeArchive, parseXml, safeCopy, safeCopyFile, unzipTo, zipTo } from '../../tools/vendor.js';
import { Cmd } from '../../tools/Cmd.js';
import { toolchain } from '../../toolchain.js';
import { BuildParams } from '../../BuildParams.js';
import { sendBuildFailureAlert } from '../../tools/alert.js';
import { ChannelCfg } from '../../typings';
import { CmdOption } from '../../CmdOption.js';
import { compareVersions } from 'compare-versions';

function mergeManifestXml(a: any, b: any, key: string = null!, bitem: any = null) {
    function findItemByName(items: {"$": {"android:name": string}}[], androidName: string) {
        for (let k=0; k < items.length; k++) {
            if (items[k]['$'] && items[k]['$']['android:name'] && items[k]['$']['android:name'] == androidName) {
                return k;
            }
        }
        return -1;
    }
   
    key = key ? key : 'manifest';
    bitem = bitem ? bitem : b[key];
    if (typeof(bitem) !== "object") {
        a[key] = bitem;
        return;
    }
    
    let isarr = (bitem instanceof Array);
    a[key] = a[key] ? a[key] : (isarr ? [] : {});
    
    if (isarr) {
        for (let k=0; k < bitem.length; k++) {
            if (bitem[k]['$'] && bitem[k]['$']['android:name']) {
                // 查找a中是否有该'android:name'
                let i = findItemByName(a[key], bitem[k]['$']['android:name'])
                if (i < 0) a[key].push(bitem[k]);
                else mergeManifestXml(a[key], bitem, String(i), bitem[k]);
            } else {
                if (typeof(bitem[k]) == "object") {
                    mergeManifestXml(a[key], bitem, String(k), null);
                }
            }
        }
    } else {
        for (let k in bitem) {
            mergeManifestXml(a[key], bitem, String(k), null);
        }
    }
}

function copyFileSync(src: string, dest: string) {
    if (fs.existsSync(src)) {
        const pd = path.dirname(dest);
        fs.ensureDirSync(pd);
        fs.copyFileSync(src, dest);
    }
}

/**
 * 把wxentry.aar的类包名com.xsdk.wxapi修改为应用的包名.wxapi
 */
class ChangeAARPackageName {
    async changes_wxentry(jartool: string, libraryDir: string, bundleId: string): Promise<void> {
        const ruleFile = path.join(libraryDir, 'rule.txt');
        const tmpPath = path.join(libraryDir, 'temp/aar');
        let aarPath = path.join(libraryDir, 'libs/wxentry.aar');
        if (fs.existsSync(aarPath)) {
            console.log("start change wxentry.aar package name");
            const aarname = path.basename(aarPath, '.aar');
            const thisTmpPath = path.join(tmpPath, aarname);
            await this.changeAarPackage(jartool, aarPath, thisTmpPath, bundleId, ruleFile, aarPath);
        }
    }

    private async changeAarPackage(jartool: string, aarpath: string, tmppath: string, bundleId: string, rulefile: string, outaarpath: string): Promise<void> {
        await unzipTo(tmppath, aarpath);

        const jarfile = path.join(tmppath, 'classes.jar');
        await this.changeJarPackage(jartool, jarfile, bundleId, rulefile);

        const manifestfile = path.join(tmppath, 'AndroidManifest.xml');
        if (fs.existsSync(manifestfile)) {
            let s = await fs.readFile(manifestfile, 'utf8');
            s = s.replace(/com.xsdk.wxapi/g, `${bundleId}.wxapi`);
            await fs.writeFile(manifestfile, s, 'utf8');
        }

        //await makeArchive(outaarpath, 'zip', tmppath);
        await zipTo(outaarpath, tmppath);
    }

    private async changeJarPackage(jartool: string, jarfile: string, bundleId: string, rulefile: string): Promise<void> {
        if (fs.existsSync(jarfile)) {
            let ruleContent = `rule com.xsdk.wxapi.** ${bundleId}.wxapi.@1\n`;
            await fs.writeFile(rulefile, ruleContent);
            await new Cmd().run('java', ['-jar', jartool, 'process', rulefile, jarfile, jarfile]);
        }
    }
}


/**
 * 将xsdkLibrary复制到gradle工程路径下
 * 替换宏定义
 */
export class CopyXsdkLibrary {
    constructor(private xsdkPath: string, private params: BuildParams, private exportproj: string, private cmdOption: CmdOption) { }
    async copy() {
        const c = toolchain.params.channelCfg;
        let exportproj = this.exportproj;

        const xsdk_channels = path.join(this.xsdkPath, 'xsdk_channels', c['channel-xsdk']);
        const xsdk_framework = path.join(this.xsdkPath, 'xsdk_framework');
        const workspace_platform = path.join(toolchain.params.workSpacePath, 'platform', c.path, 'export');

        console.log(`@@ 将xsdk_framework复制到目标, copy ${xsdk_framework} to export`);
        await safeCopy(xsdk_framework, exportproj);
        console.log(`@@ 将xsdk_channel复制到目标， copy ${xsdk_channels} to export`);
        await safeCopy(xsdk_channels, exportproj);
        console.log(`@@ 将工程的配置copy到目标， copy ${workspace_platform} to export`);
        await safeCopy(workspace_platform, exportproj);

        const bundleId = c.bundleId;
        const channelSdkParms = c['channel-sdk-params'] || {};
        channelSdkParms.engineName = this.cmdOption.engine == "laya" ? "laya" : "unity";

        //替换xml/gradle/cfg/properties中的宏定义
        const buildfiles = await findFiles(exportproj, ['.gradle', '.xml', '.properties', '.cfg', '.ini', '.json'], '+');
        console.log(buildfiles);
        for (let bf of buildfiles) {
            console.log(bf);
            let s = await fs.readFile(bf, 'utf8');

            // 查找 ${varname}
            s = s.replace(/\$\{(\w+)\}/mg, (substring: string, ...args: any[]) => {
                const macroDefine = args[0];
                return channelSdkParms[macroDefine] ?? c[<keyof ChannelCfg>macroDefine] ?? substring;
            });

            await fs.writeFile(bf, s, 'utf8');
        }

        // 将所有的cfg文件加前缀
        if (c.buildinKey) {
            const basecfgpath = path.join(exportproj, 'xsdkLibrary/src/main/assets');
            for (let cfgfile of ['fy_base_app.cfg', 'fy_xsdk_app.cfg'])
                await fs.rename(path.join(basecfgpath, cfgfile), path.join(basecfgpath, c.buildinKey + cfgfile));
        }

        // 将wxentry的包名修改为当前的应用包名
        const jartool = path.join(this.xsdkPath, '../tools/wxjartool/jarjar-1.4.jar');
        new ChangeAARPackageName().changes_wxentry(jartool, path.join(exportproj, 'xsdkLibrary'), bundleId);

        return false;
    }
}

/**
 * 将xsdkLibrary嵌入到gradle工程中
 */
export class ModifyGradle {
    constructor(private exportproj: string, private changeXSdkPackage: boolean, private bundleId: string, private appModuleName: "launcher" | "app") { }

    // 合并gradle.properties文件
    private mergeGradleProperties() {
        let launcher_gradle_s: string = fs.readFileSync(path.join(this.exportproj, this.appModuleName + '/build.gradle'), { 'encoding': 'utf-8' });

        // 合并 gradle.properties
        let prop_path = path.join(this.exportproj, 'gradle.properties');
        let prop_s = fs.readFileSync(prop_path, { 'encoding': 'utf-8' });
        prop_s += '\n' + fs.readFileSync(path.join(this.exportproj, 'xsdkLibrary/gradle_com/gradle.properties'), { 'encoding': 'utf-8' });
        prop_s += '\n' + fs.readFileSync(path.join(this.exportproj, 'xsdkLibrary/gradle_xsdk/gradle.properties'), { 'encoding': 'utf-8' });

        // 从launcher/build.gradle取出以下参数
        prop_s = prop_s.replace('${buildToolsVersion}', launcher_gradle_s.match(/buildToolsVersion\s+\'([\d\.]+)\'/)![1] as string)
            .replace('${compileSdkVersion}', launcher_gradle_s.match(/compileSdkVersion\s+(\d+)/)![1] as string)
            .replace('${targetSdkVersion}', launcher_gradle_s.match(/targetSdkVersion\s+(\d+)/)![1] as string)
            .replace('${abiFilters}', (launcher_gradle_s.match(/abiFilters\s+(.+)/)![1] as string).replace(/['\"\s]/g, ''));

        let prop = ini.parse(prop_s);

        // 将cfg.json中 channel-sdk-params 填充到 manifestPlaceholders_ 中
        if (!prop['manifestPlaceholders_']) {
            const c = toolchain.params.channelCfg;
            const channelSdkParms = c['channel-sdk-params'] || {};
            let holders = '';
            for (let k in channelSdkParms) {
                if (holders) holders += ',';
                holders += k + ':' + channelSdkParms[k];
            }
            prop['manifestPlaceholders_'] = holders;
        }

        fs.writeFileSync(prop_path, '#fybuilder merge\n' + ini.stringify(prop), { 'encoding': 'utf-8' });
        return prop;
    }

    // 修改unity导出工程的gradle文件
    private modifyGradle(xsdkMinSdkVersion: string) {

        // 把xsdkLibrary和aarSubAsset附加到gradle工程中
        fs.writeFileSync(path.join(this.exportproj, 'settings.gradle'),
            '\ninclude \':xsdkLibrary\' \ninclude \':aarSubAsset\'', { 'encoding': 'utf-8', flag: 'a' });

        fs.writeFileSync(path.join(this.exportproj, 'build.gradle'),
            'task clean2(type: Delete) {\n  delete rootProject.buildDir\n}\n\n//fybuilder add\napply from: "${rootDir}/xsdkLibrary/gradle_com/root.gradle"', { 'encoding': 'utf-8', flag: 'w' });

        let launcherPath = path.join(this.exportproj, this.appModuleName + '/build.gradle');
        let s = fs.readFileSync(launcherPath, { 'encoding': 'utf-8' });
        s = s.replace(/minSdkVersion\s+(\d+)/g, `minSdkVersion ${xsdkMinSdkVersion}`);

        fs.writeFileSync(launcherPath, s + '\n\n//fybuilder add\napply from: "${rootDir}/xsdkLibrary/gradle_com/launcher.gradle"', { 'encoding': 'utf-8' });
    }

    // 修改工程的manifest
    private async modifyManifest(launchFrom: string) {
        const manifests = await fg(np(path.join(this.exportproj, '**/AndroidManifest.xml')));
        for (let manifestPath of manifests) {
            manifestPath = manifestPath.replace(/\\/g, '/');
            const isMainXml = manifestPath.endsWith("/app/src/main/AndroidManifest.xml") /*laya*/|| manifestPath.endsWith("/unityLibrary/src/main/AndroidManifest.xml") /*unity*/
            const isAppXml = manifestPath.endsWith("/app/src/main/AndroidManifest.xml") /*laya*/|| manifestPath.endsWith("/launcher/src/main/AndroidManifest.xml") /*unity*/

            let s = fs.readFileSync(manifestPath, { 'encoding': 'utf-8' });
            let result = (await parseXml(s)) as {
                manifest: {
                    application: {
                        "$": { 'android:name': string, 'tools:replace': string },
                        activity: {
                            "$": { 'android:name': string, 'android:launchMode': string, 'android:exported': string, 'android:theme': string, 'android:hardwareAccelerated': string, 'android:windowSoftInputMode': string },
                            "intent-filter"?: { action: { "$": { 'android:name': string } }[], category: { "$": { 'android:name': string } }[] }[]
                        }[]
                    }[],
                    "uses-permission"?: [],
                }
            };

            let modifed = false;

            // 移除所有权限申请
            if (result.manifest?.["uses-permission"]) {
                delete result["manifest"]["uses-permission"];
                modifed = true;
            }

            // 修改 main.launcher activity 为 com.xsdk.unity.LaunchActivity
            if (isMainXml) {
                let com_xsdk = this.changeXSdkPackage ? `${this.bundleId}.sdk` : "com.xsdk";

                // 将xsdk_framework中的 com.xsdk.XSdkApplication 替换成 {bundleId}.sdk.XSdkApplication
                result.manifest.application[0].$ = result.manifest.application[0].$ ?? {};
                result.manifest.application[0].$['android:name'] = `${com_xsdk}.XSdkApplication`;
                //result.manifest.application[0].$['tools:replace'] = 'android:name';
                                    
                var acts = result.manifest.application[0].activity;
                for (var i = 0; acts && i < acts.length; i++) {
                    var act = acts[i];
                    var ifilters = act['intent-filter'];
                    var handled = false;
                    for (var j = 0; ifilters && j < ifilters.length; j++) {
                        var ifilter = ifilters[j];
                        if (ifilter.action[0].$['android:name'] == 'android.intent.action.MAIN'
                            && ifilter.category[0].$['android:name'] == 'android.intent.category.LAUNCHER') {

                            if (launchFrom == 'com.xsdk.unity.MainActivity') {
                                act.$['android:launchMode'] = 'singleTop';
                                act.$['android:name'] = `${com_xsdk}.unity.MainActivity`;
                                act.$['android:exported'] = 'true';
                                act.$['android:hardwareAccelerated'] = 'true';
                                act.$['android:windowSoftInputMode'] = 'adjustPan|stateHidden';
                                act.$['android:theme'] = '@android:style/Theme.Black.NoTitleBar.Fullscreen';
                            } else {
                                act.$['android:launchMode'] = 'singleTop';
                                act.$['android:name'] = launchFrom.replace(/^com\.xsdk/, com_xsdk);
                                act.$['android:exported'] = 'true';
                                act.$['android:hardwareAccelerated'] = 'true';
                                act.$['android:windowSoftInputMode'] = 'adjustPan|stateHidden';
                                act.$['android:theme'] = '@android:style/Theme.Black.NoTitleBar.Fullscreen';

                                var act2 = lodash.cloneDeep(act);
                                act2.$['android:name'] = `${com_xsdk}.unity.MainActivity`;
                                act2.$['android:launchMode'] = 'singleTask';
                                delete act2['intent-filter'];
                                acts.push(act2);
                            }

                            handled = true;

                            modifed = true;

                            break;
                        }
                    }

                    if (handled) break;
                }
            }

            if (isAppXml) {
                let customXml = await parseXml(fs.readFileSync(path.join(this.exportproj, 'xsdkLibrary/app/AndroidManifest.xml'), { 'encoding': 'utf-8' }));
                mergeManifestXml(result, customXml);
                modifed = true;
            }

            if (modifed) {
                const builder = new xml2js.Builder();
                const xml = builder.buildObject(result);
                fs.writeFileSync(manifestPath, xml, { 'encoding': 'utf-8' });
            }
        }
    }

    async insert() {
        let prop = this.mergeGradleProperties();
        this.modifyGradle(prop['xsdkMinSdkVersion']);
        this.modifyManifest(prop['xsdkLaunchFrom']);
        return prop;
    }
}


/**
 * 构建gradle工程，生成apk/aab
 */
export class GradleBuild {
    constructor(private exportproj: string, private xsdkPath: string, private gradleProperties: {[key:string]:string}, private resLibrary: 'unityLibrary' | 'app') { }
    async build(isAAB: boolean) {
        let gradlever = '';
        let gradletool = '';
        if (fs.existsSync(path.join(this.exportproj, 'gradlew' + (process.platform == 'win32' ? '.bat' : '')))) {
            gradletool = path.join(this.exportproj, 'gradlew' + (process.platform == 'win32' ? '.bat' : ''));
            let prop = ini.parse(fs.readFileSync(path.join(this.exportproj, 'gradle/wrapper/gradle-wrapper.properties'), { 'encoding': 'utf-8' }));
            gradlever = prop['distributionUrl'].match(/gradle-(\d+)/)![1] as string;
        } else {
            gradlever = this.gradleProperties['xsdkGradleToolVersion'];
            gradletool = path.join(this.xsdkPath, '../tools/gradles/' + gradlever + '/gradlew' + (process.platform == 'win32' ? '.bat' : ''));
        }

        // 兼容gradle7/8
        if (compareVersions(gradlever, "7.0.0") >= 0) {
            let launcherPath = path.join(this.exportproj, 'launcher/build.gradle');
            if (fs.existsSync(launcherPath)) {
                let s = fs.readFileSync(launcherPath, { 'encoding': 'utf-8' });
                s = s.replace(/useProguard/g, `//useProguard`);
                fs.writeFileSync(launcherPath, s, { 'encoding': 'utf-8' });
            }
            if (compareVersions(gradlever, "8.0.0") >= 0) {
                let cmd = new Cmd();
                await cmd.run("java", ["--version"]);
                let javaver = cmd.output.match(/\d+\.\d+\.\d+/)![0];
                if (compareVersions(javaver, "17.0.0") < 0) {
                    if (!fs.existsSync("C:\\Program Files\\Java\\jdk-17")) {
                        console.log("请在该位置安装java17：C:\\Program Files\\Java\\jdk-17");
                        process.exit(1);
                    }
                    process.env.JAVA_HOME = "C:\\Program Files\\Java\\jdk-17";
                }
            }
        }

        //构建aab/apk
        process.chdir(this.exportproj);

        // 构建aab包前，先构apk包
        const cmd = new Cmd();
        await cmd.run(gradletool, ['-p', this.exportproj, 'assembRelease'], { logPrefix: '[gradle]', outputFormatter:(s)=> s && s != '.' ? s : '' });
        if (!cmd.output.includes('BUILD SUCCESSFUL')) {
            await sendBuildFailureAlert('gradle 构建apk失败');
            process.exit(1);
        }

        // 如果是aab包，再把aab包构建出来
        if (isAAB) {
            // 把 unityLibrary/src/main/assets 移动到 aarSubAsset/assets, 防止aab的base包大于150M
            let src = path.join(this.exportproj, `${this.resLibrary}/src/main/assets`);
            let dst = path.join(this.exportproj, 'aarSubAsset/src/main/assets');
            let files = fs.readdirSync(src);
            for (let f of files) {
                if (f == 'bin' || ( toolchain.option.engine == "laya" && f != "cache")) continue;
                await fs.rename(path.join(src, f), path.join(dst, f));
            }

            const cmd = new Cmd();
            await cmd.run(gradletool, ['-p', this.exportproj, 'bundleRelease'], { logPrefix: '[gradle]', outputFormatter:(s)=> s && s != '.' ? s : '' });
            if (!cmd.output.includes('BUILD SUCCESSFUL')) {
                await sendBuildFailureAlert('gradle 构建aab失败');
                process.exit(1);
            }
        }
    }
}


/**
 *  将构建好的apk/aab放到 hudson下 供内网下载
 */
export class UploadApk {
    constructor(private params: BuildParams, private exportproj: string, private version: string, private appModuleName: "launcher" | "app") { }
    async upload() {
        const channelCfg = toolchain.params.channelCfg;

        // 删除上次 /res 目录下的 apk,aab
        const uploadDir = path.join(toolchain.params.uploadPath, 'apk', channelCfg.path);
        const lastapks = await fg(np(uploadDir) + '/*.(apk|aab)');
        for (let lastapk of lastapks) {
            await fs.remove(lastapk);
        }

        const name = channelCfg.apk!.replace('{version}', this.version).slice(0, -4);

        const srcapks = await fg(np(path.join(this.exportproj, this.appModuleName + '/build/outputs', '**/*.(apk|aab)')));
        for (let srcapk of srcapks) {
            // 此时不能保证apk/aab已经生成完成，检查签名是否打好
            while (true) {
                const cmd = new Cmd();
                await cmd.run("keytool", ['-printcert', '-jarfile', srcapk], { logPrefix: '[check apk signed]' });
                if (!cmd.output.includes('SHA1:')) {
                    console.log("apk的签名未完成，继续等待");
                    await wait(5000);
                } else {
                    break;
                }
            }
        }

        // 把apk(aab)复制到 /res 目录下
        await fs.ensureDir(uploadDir);
        await this.copyApkTo(uploadDir, name);

        // 将apk保存到hudson，供内网下载
        await fs.ensureDir(toolchain.params.packageLocalWebPath);
        await this.copyApkTo(toolchain.params.packageLocalWebPath, name);

        // 将symbols压缩成zip到上级目录
        const symbolsPath = path.join(this.exportproj, 'unityLibrary/symbols');
        if (fs.existsSync(symbolsPath)) {
            await makeArchive(path.join(this.exportproj, '../', `export-${this.version}.symbols.zip`), 'zip', symbolsPath);
        }

        // 将symbols统一移动到/symbols目录下
        let dstSymbolsPath = path.join(path.parse(this.exportproj).root, "symbols");
        await fs.ensureDir(dstSymbolsPath);
        const symbols = await fg(np(path.join(this.exportproj, '../*.symbols.zip')));
        for (let sf of symbols) {
            await fs.rename(sf, path.join(dstSymbolsPath, channelCfg.bundleId + '-' + path.basename(sf)));
        }

        return name;
    }

    private async copyApkTo(dstdir: string, dstname: string): Promise<void> {
        const srcapks = await fg(np(path.join(this.exportproj, this.appModuleName + '/build/outputs', '**/*.(apk|aab)')));
        for (let srcapk of srcapks) {
            let dstapk = path.join(dstdir, dstname + path.extname(srcapk));
            copyFileSync(srcapk, dstapk);
        }
    }
}


export let recordApk = async (apkName: string): Promise<void> => {
    const c = toolchain.params.channelCfg;
    const apksfile = path.join(toolchain.params.workSpacePath, 'build', 'apks.txt');
    await toolchain.svnClient.update(apksfile);
    const date = moment().format('%Y-%m-%d %H:%M:%S');
    const s = `${apkName} ${date} [gameid:${c.gameid} plat:${c.plat} ${c.platName}-${c.productName}, bundleId:${c.bundleId}, cdn:${c.url}, target:${c.targetSdkVersion}]\n`;
    await fs.appendFile(apksfile, s);
    await toolchain.svnClient.commit('auto commit apks.txt', apksfile);
    const toapksfile = path.join(toolchain.params.packageLocalWebPath, 'apks.txt');
    await safeCopyFile(apksfile, toapksfile);
}

